Skip to content

fix(objectql): insert 剥离作用于调用方提交的值,不再连坐抹掉 beforeInsert hook 的写入 (#6339) - #6611

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-6339-insert-strip-caller-values
Aug 8, 2026
Merged

fix(objectql): insert 剥离作用于调用方提交的值,不再连坐抹掉 beforeInsert hook 的写入 (#6339)#6611
baozhoutao merged 2 commits into
mainfrom
claude/issue-6339-insert-strip-caller-values

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6339

stripRuntimeOwnedFieldsbeforeInsert 之后执行,却用入口处的「键名集合」判断该删什么。hook 一旦写入 runtime-owned 列,这两件事就不是同一回事:delete result[name] 删的是执行到剥离时键上的当前值,只要调用方 payload 恰好也提交了同名键,被删掉的就是 hook 的写入。这是 update 面 #5591 / PR #6343 的 insert 面孪生缺陷,错因完全相同 —— 本 PR 是其机械镜像

关键在于:「调用方不提交该键 ⇒ hook 写入存活」正是 runtimeOwnedStripWarning() 正文对每个 hook 作者的承诺:

A beforeInsert/beforeUpdate hook does NOT need either — hook-written keys are not caller-supplied.

键名集合判据让这句话只在「调用方碰巧没提交同名键」时成立。所以这是把代码拉回它自己写下的契约,不是策略变更。

前提复核(P1-P4,动手前逐条实测)

前提 结论 证据
P1 origin/main 上 insert 面缺陷仍在 ✅ 成立 复刻正文 A/B 探针(真 ObjectQL + 内存驱动,probe_num2 { title: text, code: autonumber },hook 赋值 ctx.input.data.code):PROBE A code = "HOOK-OVERWRITE" / PROBE B code = "1"#6343 未顺带盖住 insert,premise_still_valid: true
P2 #6343 落地形状可考且可镜像 ✅ 成立 读 squash 9c82b8990:快照构造点 engine.ts update 终端顶部(hook 之前)、比对点 Object.is + hasOwnProperty、非导出签名 ReadonlySet< string >Readonly< Record< string, unknown > >。三处逐项镜像见下表
P3 suppliedPerRow(取自 opCtx.data)与 hook 原地改的 ctx.input.data 是否同引用 实测:不同引用 PROBE aliasing A/B: ctx.input.data === callerPayload ? false;插入后调用方对象未被改动({"title":"B","code":"CALLER-FORGED"})。原因:applyFieldDefaults 返回 { ...record }(engine.ts:1993)。但这是巧合而非不变量:该函数在 !fields 提前返回时原样交回同一引用(engine.ts:1988),initializeSummaryFields 也只在真正播种时复制。所以快照仍显式浅拷贝、并提前到 hook 之前取 —— 与 update 面 #6343 spread 的理由相同,只是那边拷贝是止血、这边是加固
P4 #6553(播种段)与本单(insert 剥离段)无纠缠 ✅ 成立 #6553 = 6f6fec72d,已在本分支 base 内;其 engine.ts hunk 全部落在 @@ 2130/2140/2183/2203(seedAutonumber 一带),本 PR 落在 5590-5720(insert 剥离一带)。merge origin/main 后 git merge-tree --write-tree 干净退出

#6343 的镜像对照

update(#5591 / PR #6343,已合) insert(本 PR)
剥离函数 stripReadonlyFields stripRuntimeOwnedFields
第三参签名 suppliedKeys: ReadonlySet< string >supplied: Readonly< Record< string, unknown > > 同(镜像)
键存在判据 Object.prototype.hasOwnProperty.call(supplied, name)(不用 in,constructor 是合法字段名) 同(镜像)
值身份判据 Object.is(result[name], supplied[name])(不用 ===,伪造 NaN 会被 === 判成「hook 改过」而留存) 同(镜像)
快照 suppliedValues = { ...opCtx.data },取于 update 终端顶部、hook 之前 suppliedPerRow = rows.map(r => ({ ...r })),提前到 triggerHooks('beforeInsert') 之前
调用点数 2(单 id 路径 + 谓词 multi 路径) 1(单行与批量共用同一 per-row 循环)
豁免 isSystem / preserveAudit 未触碰
已知限制 浅快照:hook 原地改调用方传入的对象无法与「什么都没做」区分,仍被剥(fail-safe) 同,并注明 autonumber 值在所有受支持形态下都是标量,故此限制在本面是理论性的

⛔ 未发明第二套形状:判据、拼写、注释结构均照搬 #6343

反向验证(方向先写死,再实测)

预测(回退为键名集合判据 = 删掉 Object.is 一行,保留 own-key 判断):② 类用例转红,①③ 类保持绿。预测清单写在实施前。

实测:11 红,与预测逐条一致,无额外红绿。

预测 实测 用例
🔴 🔴 B (THE REPORT) — 落库回到 "1"
🔴 🔴 A and B now agree
🔴 🔴 a hook-overwritten code produces NO warning
🔴 🔴 P3: the caller-value snapshot is NOT the object the hook mutates in place
🔴 🔴 a hook that REPLACES ctx.input.data wholesale(键集仍含 code,替换后的值一并被删)
🔴 🔴 BULK: one batch, mixed rows
🔴 🔴 BULK: a caller-supplied value is never read from the WRONG row
🔴 🔴 onFieldsDropped: silent for a hook-overwritten code
🔴 🔴 strictReadonlyWrites refuses the real forge and admits the hook write
🔴 🔴 单测 #6339 > KEEPS a record number a hook OVERWROTE
🔴 🔴 单测 #6339 > warns only for the value it really dropped
🟢 🟢 A (control) / #5503 UNCHANGED / isSystem / preserveAudit / hook can still SEE
🟢 🟢 单测 #5503 全组 4 条;#6339STILL drops when unchanged / NaN / Object.prototype key

回退实测输出:Tests 11 failed | 151 passed (162)。恢复后全绿。

测试

新测试落专用文件 packages/objectql/src/engine-insert-runtime-owned-strip.test.ts(15 例,⛔ 未触 engine.test.ts),另在 rule-validator.test.ts#6339 单测组(5 例)并把既有 4 条 #5503 用例改成值对象形态(签名变更所致,与 #6343 对 update 单测的改法一致)。

覆盖:① A 用例 pin(hook 新增键存活)② B 用例翻正 ③ 调用方值确实被剥 + 警告文案不变 ④ P3 快照不被 hook 原地改污染 ⑤ bulk 多行混合形态(新增/覆写/未覆写/空)+ 跨行取值防串行,以及 onFieldsDropped / strictReadonlyWrites / isSystem / preserveAudit / hook 仍可读到调用方原值。

$ pnpm --workspace-concurrency=2 --filter @objectstack/objectql typecheck
> tsc --noEmit                                    # 无输出 = 绿

$ pnpm --workspace-concurrency=2 --filter @objectstack/objectql test   # merge origin/main 之后
 Test Files  149 passed (149)
      Tests  2514 passed (2514)

$ pnpm check:engine-double-contract
check-engine-double-contract: OK — 94 pinned, 133 in the DEBT ledger, 2 exempt.

$ pnpm check:query-options-erasure
✓ query-options-erasure ratchet holds: 77 unswept non-test site(s) in 18 file(s), none new.
  test surface: 263 site(s) in 49 file(s) — at the ceiling.      # 新测试文件新增 0 个擦除点,⛔ 未用 `{} as any`

$ pnpm check:type-check-debt                       # 先全量 turbo build(71 tasks successful)
check-type-check-coverage: OK — 62/77 workspace packages type-checked …
check-type-check-coverage --re-measure: OK — 34 ledger entr(ies) re-measured, none above its recorded number.
                                                   # 台账未抬;既有 surplus 与本 PR 无关,未动

$ node scripts/check-nul-bytes.mjs                 # 绿;另对本 PR 全部改动文件自扫
$ grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' -- (本 PR 改动的全部文件)   # 无命中

边界


Generated by Claude Code

claude added 2 commits August 8, 2026 06:57
`stripRuntimeOwnedFields` 在 `beforeInsert` 之后执行,却用入口处的「键名集合」
判断该删什么。hook 一旦写入 runtime-owned 列,这两件事就不是同一回事:
`delete result[name]` 删的是执行到剥离时键上的当前值,只要调用方 payload 恰好
也提交了同名键,被删掉的就是 hook 的写入。这是 update 面 #5591 / PR #6343 的
insert 面孪生缺陷,错因完全相同。

实测(真 ObjectQL + 内存驱动,对象 `{ title: text, code: autonumber }`,
hook 赋值 `ctx.input.data.code`):

- 调用方不提交 `code` ⇒ 落库为 hook 的值(hook 写入存活)
- 调用方提交 `code`   ⇒ 落库为 `"1"`,序列值(hook 写入被删)

两次调用的差别只有「调用方 payload 里是否碰巧出现同名键」,而前者正是
`runtimeOwnedStripWarning()` 正文对每个 hook 作者的承诺:「A beforeInsert/
beforeUpdate hook does NOT need either — hook-written keys are not
caller-supplied」。所以这是把代码拉回它自己写下的契约,不是策略变更。

入口快照改为携带调用方的值,并显式浅拷贝、且提前到 hook 之前取:hook 原地改
`ctx.input.data` 不会篡改「调用方发了什么」的记录。实测 origin/main 上两者本
就不同引用(`applyFieldDefaults` 返回 `{ ...record }`),但它的 `!fields` 提前
返回会原样交回同一引用,`initializeSummaryFields` 也只在播种时复制 —— 拷贝把
这份「三处调用点的巧合」变成本路径自身的不变量。

不放松 #5503:没有 hook 覆写的调用方 seed 依旧被剥,单行与批量两条路径一致,
警告文案、`onFieldsDropped` 事件与 `strictReadonlyWrites` 拒绝均不变;
`isSystem` / `preserveAudit` 未触碰。比对用 `Object.is` 而非 `===`,调用方伪造
的 `NaN` 仍被认定为调用方自己的值并剥离。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 7:06am

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 8, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 8, 2026 07:19
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit bfe689b Aug 8, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-6339-insert-strip-caller-values branch August 8, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants